

#include <processing/introspection_query_handler.hpp>
#include <util/crc_hash.hpp>
#include <struct/context_operations.hpp>
#include <data/data_result.hpp>

using namespace std::string_view_literals;

namespace blitz_query_cpp
{
    bool introspection_query_handler::process(query_context &context)
    {
        if (!context.document.operation)
            return true;

        if (context.document.operation->operation_type != operation_type_t::Query)
            return true;

        auto object = get_root_selection_node(context);
        if (object == nullptr)
            return true;

        switch (hash_crc32(object->name))
        {
        case "__type"_crc32:
        {
            if (!check_introspection(context))
                return false;
            auto type_name_arg = object->arguments.find("name");
            if (!type_name_arg)
            {
                context.report_error("'name' argument required for __type query");
                return false;
            }
            data_result_t &root_node = get_data_result(context);
            auto &data = root_node.add_child_node(value_kind::Object, "data");
            std::string_view type_name = type_name_arg->children[0]->content;
            type_reference type_ref{type_name};
            type_ref.type = context.schema->find_type(type_name);
            process_type(context, type_ref, object->selection_set, data);
        }
        break;
        case "__schema"_crc32:
            if (!check_introspection(context))
                return false;
            process_schema(context, object->selection_set);
            break;
        case "__typename"_crc32:
        {
            if (!check_introspection(context))
                return false;
            data_result_t &root_node = get_data_result(context);
            auto &data = root_node.add_child_node(value_kind::Object, "data");
            data.add_child_node(value_kind::String, object->get_output_name(), context.schema->query_type_name);
        }
        break;
        default:
            return true;
        }

        return true;
    }

    bool introspection_query_handler::process_schema(query_context &context, syntax_node *selection_set)
    {
        data_result_t &root_node = get_data_result(context);
        auto &output_node = root_node.add_child_node(value_kind::Object, "data");

        for (const syntax_node *field_node : selection_set->children)
        {
            switch (hash_crc32(field_node->name))
            {
            case "description"_crc32:
                output_node.add_child_node(value_kind::String, field_node->get_output_name(), context.schema->get_description());
                break;
            case "types"_crc32:
            {
                auto &types_output_node = output_node.add_child_node(value_kind::List, field_node->get_output_name());
                for (auto &&type : context.schema->types)
                {
                    type_reference type_ref{type.name};
                    type_ref.type = &type;
                    if (!process_type(context, type_ref, field_node->selection_set, types_output_node))
                        return false;
                }
            }
            break;
            case "queryType"_crc32:
            {
                type_reference type_ref{context.schema->query_type_name};
                type_ref.type = context.schema->get_query_type();
                if (type_ref.type == nullptr)
                {
                    output_node.add_child_node(value_kind::Null, field_node->get_output_name());
                    break;
                }
                auto &types_output_node = output_node.add_child_node(value_kind::Object, field_node->get_output_name());
                if (!process_type(context, type_ref, field_node->selection_set, types_output_node))
                    return false;
            }
            break;
            case "mutationType"_crc32:
            {
                type_reference type_ref{context.schema->mutation_type_name};
                type_ref.type = context.schema->get_mutation_type();
                if (type_ref.type == nullptr)
                {
                    output_node.add_child_node(value_kind::Null, field_node->get_output_name());
                    break;
                }
                auto &types_output_node = output_node.add_child_node(value_kind::Object, field_node->get_output_name());
                if (!process_type(context, type_ref, field_node->selection_set, types_output_node))
                    return false;
            }
            break;
            case "subscriptionType"_crc32:
            {
                type_reference type_ref{context.schema->subscription_type_name};
                type_ref.type = context.schema->get_subscription_type();
                if (type_ref.type == nullptr)
                {
                    output_node.add_child_node(value_kind::Null, field_node->get_output_name());
                    break;
                }
                auto &types_output_node = output_node.add_child_node(value_kind::Object, field_node->get_output_name());
                if (!process_type(context, type_ref, field_node->selection_set, types_output_node))
                    return false;
            }
            break;
            case "directives"_crc32:
            {
                auto &directives_output_node = output_node.add_child_node(value_kind::List, field_node->get_output_name());
                for (auto &&directive : context.schema->directive_types)
                {
                    if (!process_directive(context, directive, field_node->selection_set, directives_output_node))
                        return false;
                }
            }
            break;
            default:
                return context.report_error("Field {} is not fedined in type {}", field_node->name, "__Schema");
            }
        }
        return true;
    }

    bool introspection_query_handler::process_directive(query_context &context, const directive_type &dir, const syntax_node *selection_set, value_node_t &output_node)
    {
        auto &directive_output_node = output_node.add_child_node(value_kind::Object);
        for (const syntax_node *field_node : selection_set->children)
        {
            switch (hash_crc32(field_node->name))
            {
            case "name"_crc32:
                directive_output_node.add_child_node(value_kind::String, field_node->get_output_name(), dir.name);
                break;
            case "description"_crc32:
                directive_output_node.add_child_node(value_kind::String, field_node->get_output_name(), dir.description);
                break;
            case "locations"_crc32:
            {
                auto &locations = directive_output_node.add_child_node(value_kind::List, field_node->get_output_name());
                for (directive_target_t t = directive_target_t::Query; t != directive_target_t::LastValue; t = t << 1)
                {
                    if (has_any_flag(dir.target, t))
                        locations.add_child_node(value_kind::Enum, field_node->get_output_name(), enum_name(t));
                }
            }
            break;
            case "isRepeatable"_crc32:
                directive_output_node.add_child_node_bool(value_kind::Boolean, field_node->get_output_name(),
                                                          has_any_flag(dir.target, directive_target_t::IsRepeatable));
                break;
            case "args"_crc32:
            {
                auto &arg_output = directive_output_node.add_child_node(value_kind::List, field_node->get_output_name());
                for (auto &&arg : dir.arguments)
                {
                    if (!process_input_field(context, arg, field_node->selection_set, arg_output))
                        return false;
                }
            }
            break;
            default:
                return context.report_error("Field {} is not fedined in type {}", field_node->name, "__Field");
            }
        }
        return true;
    }

    bool introspection_query_handler::process_field(query_context &context, const field &field_decl, const syntax_node *selection_set, value_node_t &output_node)
    {
        auto &field_output_node = output_node.add_child_node(value_kind::Object);
        for (const syntax_node *field_node : selection_set->children)
        {
            switch (hash_crc32(field_node->name))
            {
            case "name"_crc32:
                field_output_node.add_child_node(value_kind::String, field_node->get_output_name(), field_decl.name);
                break;
            case "description"_crc32:
                field_output_node.add_child_node(value_kind::String, field_node->get_output_name(), field_decl.description);
                break;
            case "type"_crc32:
            {
                auto &filed_type_output = field_output_node.add_child_node(value_kind::Object, field_node->get_output_name());
                if (!process_type(context, field_decl.field_type, field_node->selection_set, filed_type_output))
                    return false;
            }
            break;
            case "deprecationReason"_crc32:
                field_output_node.add_child_node(value_kind::String, field_node->get_output_name(), field_decl.deprecation_reason);
                break;
            case "isDeprecated"_crc32:
                field_output_node.add_child_node_bool(value_kind::Boolean, field_node->get_output_name(), field_decl.is_deprecated);
                break;
            case "args"_crc32:
            {
                auto &filed_type_output = field_output_node.add_child_node(value_kind::List, field_node->get_output_name());
                for (auto &&arg : field_decl.arguments)
                {
                    if (!process_input_field(context, arg, field_node->selection_set, filed_type_output))
                        return false;
                }
            }
            break;
            default:
                return context.report_error("Field {} is not fedined in type {}", field_node->name, "__Field");
            }
        }
        return true;
    }

    bool introspection_query_handler::process_input_field(query_context &context, const input_value &field_decl, const syntax_node *selection_set, value_node_t &output_node)
    {
        auto &field_output_node = output_node.add_child_node(value_kind::Object);
        for (const syntax_node *field_node : selection_set->children)
        {
            switch (hash_crc32(field_node->name))
            {
            case "name"_crc32:
                field_output_node.add_child_node(value_kind::String, field_node->get_output_name(), field_decl.name);
                break;
            case "description"_crc32:
                field_output_node.add_child_node(value_kind::String, field_node->get_output_name(), field_decl.description);
                break;
            case "defaultValue"_crc32:
                field_output_node.add_child_node(value_kind::String, field_node->get_output_name(), field_decl.default_value.to_string());
                break;
            case "type"_crc32:
            {
                auto &filed_type_output = field_output_node.add_child_node(value_kind::Object, field_node->get_output_name());
                if (!process_type(context, field_decl.field_type, field_node->selection_set, filed_type_output))
                    return false;
            }
            break;
            default:
                return context.report_error("Field {} is not fedined in type {}", field_node->name, "__InputValue");
            }
        }
        return true;
    }

    bool introspection_query_handler::process_type(query_context &context, const type_reference &type_ref, const syntax_node *selection_set, value_node_t &output_node)
    {
        const object_type *type = type_ref.type;
        if (!type)
        {
            return context.report_error("Type '{}' is not found in schema", type_ref.name);
        }
        const object_type *type_type = context.schema->get_type_type();

        for (const syntax_node *field_node : selection_set->children)
        {
            const auto &type_filed_def = type_type->fields.find(field_node->name);
            if (type_filed_def == type_type->fields.end())
                return context.report_error("Type '{}' has no field {}", type_type->name, field_node->name);

            std::string_view output_name = field_node->get_output_name();
            switch (hash_crc32(field_node->name))
            {
            case "kind"_crc32:
                if (type_ref.not_nullable & 0x1)
                    output_node.add_child_node(value_kind::Enum, output_name, "NON_NULL"sv);
                else if (type_ref.list_nesting_depth > 0)
                    output_node.add_child_node(value_kind::Enum, output_name, "LIST"sv);
                else
                    output_node.add_child_node(value_kind::Enum, output_name, enum_name(type->kind));
                break;
            case "name"_crc32:
                if (type_ref.not_nullable & 0x1 || type_ref.list_nesting_depth > 0)
                {
                    output_node.add_child_node(value_kind::Null, output_name);
                    break;
                }
                output_node.add_child_node(value_kind::String, output_name, type->name);
                break;
            case "description"_crc32:
                if (type_ref.not_nullable & 0x1 || type_ref.list_nesting_depth > 0)
                {
                    output_node.add_child_node(value_kind::Null, output_name);
                    break;
                }
                output_node.add_child_node(value_kind::String, output_name, type->description);
                break;
            case "specifiedByURL"_crc32:
            {
                if (type_ref.not_nullable & 0x1 || type_ref.list_nesting_depth > 0)
                {
                    output_node.add_child_node(value_kind::Null, output_name);
                    break;
                }
                auto specifiedByDir = type->find_directive("specifiedBy");
                if (!specifiedByDir)
                {
                    output_node.add_child_node(value_kind::Null, output_name);
                    break;
                }
                auto url_param = specifiedByDir->parameters.find("url");
                if (url_param == specifiedByDir->parameters.end())
                {
                    output_node.add_child_node(value_kind::Null, output_name);
                    break;
                }
                output_node.add_child_node(value_kind::String, output_name, url_param->string_value);
            }
            break;
            case "fields"_crc32:
            {
                if (type_ref.not_nullable & 0x1 || type_ref.list_nesting_depth > 0)
                {
                    output_node.add_child_node(value_kind::Null, output_name);
                    break;
                }
                if (!field_node->selection_set || field_node->selection_set->children.size() == 0)
                    return context.report_error("Composite type {} should have non empty selection set", type_filed_def->field_type.name);

                auto &fields_output_node = output_node.add_child_node(value_kind::List, output_name);
                if (type->kind != type_kind::Object)
                    break;
                for (auto &&field_decl : type->fields)
                {
                    if (!process_field(context, field_decl, field_node->selection_set, fields_output_node))
                        break;
                }
            }
            break;
            case "inputFields"_crc32:
            {
                if (type_ref.not_nullable & 0x1 || type_ref.list_nesting_depth > 0)
                {
                    output_node.add_child_node(value_kind::Null, output_name);
                    break;
                }
                if (!field_node->selection_set || field_node->selection_set->children.size() == 0)
                    return context.report_error("Composite type {} should have non empty selection set", type_filed_def->field_type.name);

                auto &fields_output_node = output_node.add_child_node(value_kind::List, output_name);
                if (type->kind != type_kind::InputObject)
                    break;
                for (auto &&field_decl : type->fields)
                {
                    if (!process_input_field(context, field_decl, field_node->selection_set, fields_output_node))
                        break;
                }
            }
            break;
            case "interfaces"_crc32:
            {
                if (type_ref.not_nullable & 0x1 || type_ref.list_nesting_depth > 0)
                {
                    output_node.add_child_node(value_kind::Null, output_name);
                    break;
                }
                if (!field_node->selection_set || field_node->selection_set->children.size() == 0)
                    return context.report_error("Composite type {} should have non empty selection set", type_filed_def->field_type.name);

                auto &interfaces_output_node = output_node.add_child_node(value_kind::List, output_name);
                if (type->kind != type_kind::Object)
                    break;
                for (auto &&interface_type_ref : type->implements)
                {
                    if (!process_type(context, interface_type_ref, field_node->selection_set, interfaces_output_node))
                        return false;
                }
            }
            break;

            case "possibleTypes"_crc32:
            {
                if (type_ref.not_nullable & 0x1 || type_ref.list_nesting_depth > 0)
                {
                    output_node.add_child_node(value_kind::Null, output_name);
                    break;
                }
                if (!field_node->selection_set || field_node->selection_set->children.size() == 0)
                    return context.report_error("Composite type {} should have non empty selection set", type_filed_def->field_type.name);

                auto &interfaces_output_node = output_node.add_child_node(value_kind::List, output_name);
                if (type->kind != type_kind::Union)
                    break;
                for (auto &&interface_type_ref : type->implements)
                {
                    if (!process_type(context, interface_type_ref, field_node->selection_set, interfaces_output_node))
                        return false;
                }
            }
            break;
            case "enumValues"_crc32:
            {
                if (type_ref.not_nullable & 0x1 || type_ref.list_nesting_depth > 0)
                {
                    output_node.add_child_node(value_kind::Null, output_name);
                    break;
                }
                if (!field_node->selection_set || field_node->selection_set->children.size() == 0)
                    return context.report_error("Composite type {} should have non empty selection set", type_filed_def->field_type.name);

                auto &interfaces_output_node = output_node.add_child_node(value_kind::List, output_name);
                if (type->kind != type_kind::Union)
                    break;
                for (auto &&interface_type_ref : type->fields)
                {
                    interfaces_output_node.add_child_node(value_kind::Enum, interface_type_ref.name);
                }
            }
            break;
            case "ofType"_crc32:
            {
                if (!field_node->selection_set || field_node->selection_set->children.size() == 0)
                    return context.report_error("Composite type {} should have non empty selection set", type_filed_def->field_type.name);
                if (type_ref.not_nullable == 0 && type_ref.list_nesting_depth == 0)
                {
                    output_node.add_child_node(value_kind::Null, output_name);
                    break;
                }
                auto &of_type_output_node = output_node.add_child_node(value_kind::Object, output_name);
                type_reference nested_type_ref = type_ref; // copy
                if (nested_type_ref.not_nullable & 0x1)
                {
                    nested_type_ref.not_nullable &= ~0x1;
                }
                else if (nested_type_ref.list_nesting_depth > 0)
                {
                    nested_type_ref.list_nesting_depth--;
                    nested_type_ref.not_nullable >>= 1;
                }
                if (!process_type(context, nested_type_ref, field_node->selection_set, of_type_output_node))
                    return false;
            }
            break;
            default:
                return context.report_error("Field {} is not fedined in type {}", field_node->name, "__Type");
            }
        }

        return true;
    }
}
